Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #199 +/- ##
==========================================
- Coverage 97.87% 97.63% -0.25%
==========================================
Files 32 32
Lines 3296 3296
==========================================
- Hits 3226 3218 -8
- Misses 70 78 +8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Benchmark Results (minimum time) — ubuntu-latestReporting the minimum over all samples (robust to shared-runner contention), not the median. Alternative backends vs Float32 (
|
| Scenario | F32 | I16 | 1b | 2b | ×I16 | ×1b | ×2b |
|---|---|---|---|---|---|---|---|
| 4-antenna @ 5 MHz | 12.2 μs | 6.09 μs | 5.23 μs | 9.74 μs | 2.01 ✅ | 2.34 ✅ | 1.26 ✅ |
| GPS L1CA, 8 sats @ 40 MHz | 331.0 μs | 136.0 μs | 71.1 μs | 120.0 μs | 2.43 ✅ | 4.66 ✅ | 2.77 ✅ |
| GPS L1CA, 8 sats @ 5 MHz | 49.1 μs | 28.2 μs | 20.1 μs | 27.9 μs | 1.74 ✅ | 2.44 ✅ | 1.76 ✅ |
| Galileo E1B, 4 sats @ 25 MHz | 123.0 μs | 55.2 μs | 2.22 ✅ | ||||
| dynamic taps @ 5 MHz (kernel) | 5.78 μs | 3.4 μs | 2.04 μs | 2.81 μs | 1.7 ✅ | 2.84 ✅ | 2.06 ✅ |
| multi-signal N=3 @ 5 MHz | 9.53 μs | 5.84 μs | 5.01 μs | 7.09 μs | 1.63 ✅ | 1.9 ✅ | 1.34 ✅ |
Time benchmarks (base vs PR head)
Ratio = 99d37e1… / 16f259c…: >1 means the PR is faster. ✅ ≥ 5 % faster,
| 99d37e1… | 16f259c… | 99d37e1… / 16f259c… | |
|---|---|---|---|
| downconvert and correlate/CPU/Float32 | 2.34 μs | 2.34 μs | 0.998 |
| downconvert and correlate/CPU/Float32 4ant | 4.72 μs | 4.74 μs | 0.995 |
| downconvert and correlate/CPU/Float64 | 2.76 μs | 2.76 μs | 0.998 |
| downconvert and correlate/CPU/Int16 | 2.42 μs | 2.42 μs | 1.0 |
| downconvert and correlate/CPU/Int16 4ant | 5.19 μs | 5.2 μs | 0.997 |
| downconvert and correlate/CPU/Int32 | 2.41 μs | 2.41 μs | 0.998 |
| fused kernel/1-ant dynamic taps | 2.16 μs | 2.16 μs | 1.0 |
| fused kernel/1-ant static taps | 1.88 μs | 1.88 μs | 1.0 |
| fused kernel/4-ant dynamic taps | 5.44 μs | 5.36 μs | 1.01 |
| fused kernel/4-ant static taps | 4.13 μs | 4.16 μs | 0.994 |
| fused tuple kernel/1-ant N=2 | 2.35 μs | 2.35 μs | 1.0 |
| fused tuple kernel/1-ant N=3 | 2.85 μs | 2.82 μs | 1.01 |
| fused tuple kernel/2-ant N=2 | 3.73 μs | 3.6 μs | 1.04 |
| fused tuple kernel/2-ant N=3 | 4.58 μs | 4.55 μs | 1.01 |
| fused tuple kernel/4-ant N=2 | 6.0 μs | 6.11 μs | 0.981 |
| fused tuple kernel/4-ant N=3 | 9.73 μs | 9.78 μs | 0.995 |
| track/1. Float32/2K – track | 2.57 μs | 2.57 μs | 1.0 |
| track/1. Float32/2K – track! | 2.66 μs | 2.65 μs | 1.0 |
| track/2. L1 8sat/5K – track | 48.0 μs | 47.7 μs | 1.01 |
| track/2. L1 8sat/5K – track! | 47.3 μs | 47.3 μs | 1.0 |
| track/2. L1 8sat/5K – track! Int16 | 29.5 μs | 27.9 μs | 1.06 ✅ |
| track/2. L1 8sat/5K – track! OneBit | 20.1 μs | 20.1 μs | 1.0 |
| track/2. L1 8sat/5K – track!-threaded | 47.4 μs | 47.1 μs | 1.01 |
| track/2. L1 8sat/5K – track-threaded | 48.0 μs | 48.7 μs | 0.986 |
| track/3. E1B 4sat/25K – track | 119.0 μs | 118.0 μs | 1.01 |
| track/3. E1B 4sat/25K – track! | 118.0 μs | 117.0 μs | 1.01 |
| track/3. E1B 4sat/25K – track! Int16 | 55.3 μs | 54.9 μs | 1.01 |
| track/3. E1B 4sat/25K – track!-threaded | 118.0 μs | 117.0 μs | 1.01 |
| track/3. E1B 4sat/25K – track-threaded | 119.0 μs | 118.0 μs | 1.01 |
| track/4. 8L1+8E1B/25K – track | 436.0 μs | 435.0 μs | 1.0 |
| track/4. 8L1+8E1B/25K – track! | 435.0 μs | 432.0 μs | 1.01 |
| track/4. 8L1+8E1B/25K – track!-threaded | 435.0 μs | 434.0 μs | 1.0 |
| track/4. 8L1+8E1B/25K – track-threaded | 436.0 μs | 435.0 μs | 1.0 |
| track/5. multi-signal N=1/5K – track | 6.14 μs | 6.09 μs | 1.01 |
| track/5. multi-signal N=1/5K – track! | 5.89 μs | 6.31 μs | 0.934 |
| track/6. multi-signal N=2/5K – track | 8.37 μs | 8.35 μs | 1.0 |
| track/6. multi-signal N=2/5K – track! | 8.51 μs | 8.5 μs | 1.0 |
| track/7. multi-signal N=3/5K – track | 10.7 μs | 10.7 μs | 1.01 |
| track/7. multi-signal N=3/5K – track! | 11.0 μs | 10.9 μs | 1.01 |
| track/8. L1CA presync 2 blk – track! | 11.6 μs | 11.6 μs | 1.01 |
| track/8. L1CA presync 20 blk – track! | 113.0 μs | 113.0 μs | 1.0 |
| track/8. L1CA synced 2 blk – track! | 11.6 μs | 11.5 μs | 1.01 |
| track/8. L1CA synced 20 blk – track! | 112.0 μs | 111.0 μs | 1.01 |
| time_to_load | 99.4 μs | 109.0 μs | 0.913 |
Memory benchmarks (base vs PR head)
| 99d37e1… | 16f259c… | |
|---|---|---|
| downconvert and correlate/CPU/Float32 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Float32 4ant | 2 allocs: 848 B | 2 allocs: 848 B |
| downconvert and correlate/CPU/Float64 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Int16 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Int16 4ant | 2 allocs: 848 B | 2 allocs: 848 B |
| downconvert and correlate/CPU/Int32 | 2 allocs: 576 B | 2 allocs: 576 B |
| fused kernel/1-ant dynamic taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/1-ant static taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/4-ant dynamic taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/4-ant static taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/1-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/1-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/2-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/2-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/4-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/4-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| track/1. Float32/2K – track | 9 allocs: 944 B | 9 allocs: 944 B |
| track/1. Float32/2K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track | 10 allocs: 4536 B | 10 allocs: 4536 B |
| track/2. L1 8sat/5K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track! Int16 | 13 allocs: 1056 B | 13 allocs: 1056 B |
| track/2. L1 8sat/5K – track! OneBit | 45 allocs: 2736 B | 45 allocs: 2736 B |
| track/2. L1 8sat/5K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track-threaded | 10 allocs: 4536 B | 10 allocs: 4536 B |
| track/3. E1B 4sat/25K – track | 10 allocs: 2808 B | 10 allocs: 2808 B |
| track/3. E1B 4sat/25K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/3. E1B 4sat/25K – track! Int16 | 5 allocs: 416 B | 5 allocs: 416 B |
| track/3. E1B 4sat/25K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/3. E1B 4sat/25K – track-threaded | 10 allocs: 2808 B | 10 allocs: 2808 B |
| track/4. 8L1+8E1B/25K – track | 26 allocs: 10352 B | 26 allocs: 10352 B |
| track/4. 8L1+8E1B/25K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/4. 8L1+8E1B/25K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/4. 8L1+8E1B/25K – track-threaded | 26 allocs: 10352 B | 26 allocs: 10352 B |
| track/5. multi-signal N=1/5K – track | 9 allocs: 944 B | 9 allocs: 944 B |
| track/5. multi-signal N=1/5K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/6. multi-signal N=2/5K – track | 9 allocs: 1408 B | 9 allocs: 1408 B |
| track/6. multi-signal N=2/5K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/7. multi-signal N=3/5K – track | 9 allocs: 1760 B | 9 allocs: 1760 B |
| track/7. multi-signal N=3/5K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/8. L1CA presync 2 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| track/8. L1CA presync 20 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| track/8. L1CA synced 2 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| track/8. L1CA synced 20 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| time_to_load | 145 allocs: 11216 B | 145 allocs: 11216 B |
Benchmark Results (minimum time) — macos-14Reporting the minimum over all samples (robust to shared-runner contention), not the median. Alternative backends vs Float32 (
|
| Scenario | F32 | I16 | 1b | 2b | ×I16 | ×1b | ×2b |
|---|---|---|---|---|---|---|---|
| 4-antenna @ 5 MHz | 11.8 μs | 5.81 μs | 3.97 μs | 8.67 μs | 2.04 ✅ | 2.98 ✅ | 1.37 ✅ |
| GPS L1CA, 8 sats @ 40 MHz | 394.0 μs | 123.0 μs | 64.2 μs | 113.0 μs | 3.21 ✅ | 6.14 ✅ | 3.49 ✅ |
| GPS L1CA, 8 sats @ 5 MHz | 56.6 μs | 23.4 μs | 16.9 μs | 25.3 μs | 2.42 ✅ | 3.35 ✅ | 2.24 ✅ |
| Galileo E1B, 4 sats @ 25 MHz | 141.0 μs | 58.8 μs | 2.4 ✅ | ||||
| dynamic taps @ 5 MHz (kernel) | 7.17 μs | 2.38 μs | 1.55 μs | 2.64 μs | 3.02 ✅ | 4.61 ✅ | 2.71 ✅ |
| multi-signal N=3 @ 5 MHz | 11.2 μs | 5.24 μs | 3.87 μs | 6.34 μs | 2.15 ✅ | 2.91 ✅ | 1.77 ✅ |
Time benchmarks (base vs PR head)
Ratio = 99d37e1… / 16f259c…: >1 means the PR is faster. ✅ ≥ 5 % faster,
| 99d37e1… | 16f259c… | 99d37e1… / 16f259c… | |
|---|---|---|---|
| downconvert and correlate/CPU/Float32 | 2.53 μs | 2.48 μs | 1.02 |
| downconvert and correlate/CPU/Float32 4ant | 4.86 μs | 5.01 μs | 0.971 |
| downconvert and correlate/CPU/Float64 | 2.7 μs | 2.8 μs | 0.965 |
| downconvert and correlate/CPU/Int16 | 2.62 μs | 2.64 μs | 0.991 |
| downconvert and correlate/CPU/Int16 4ant | 4.97 μs | 4.82 μs | 1.03 |
| downconvert and correlate/CPU/Int32 | 2.71 μs | 2.56 μs | 1.06 ✅ |
| fused kernel/1-ant dynamic taps | 2.74 μs | 2.74 μs | 1.0 |
| fused kernel/1-ant static taps | 2.2 μs | 2.2 μs | 1.0 |
| fused kernel/4-ant dynamic taps | 8.33 μs | 8.11 μs | 1.03 |
| fused kernel/4-ant static taps | 4.75 μs | 4.76 μs | 0.999 |
| fused tuple kernel/1-ant N=2 | 3.39 μs | 3.51 μs | 0.967 |
| fused tuple kernel/1-ant N=3 | 3.65 μs | 3.65 μs | 1.0 |
| fused tuple kernel/2-ant N=2 | 6.77 μs | 6.76 μs | 1.0 |
| fused tuple kernel/2-ant N=3 | 6.02 μs | 6.02 μs | 1.0 |
| fused tuple kernel/4-ant N=2 | 12.4 μs | 12.4 μs | 1.0 |
| fused tuple kernel/4-ant N=3 | 10.8 μs | 10.8 μs | 1.0 |
| track/1. Float32/2K – track | 2.76 μs | 2.77 μs | 0.998 |
| track/1. Float32/2K – track! | 2.94 μs | 2.84 μs | 1.03 |
| track/2. L1 8sat/5K – track | 51.4 μs | 53.3 μs | 0.965 |
| track/2. L1 8sat/5K – track! | 52.6 μs | 53.0 μs | 0.993 |
| track/2. L1 8sat/5K – track! Int16 | 21.3 μs | 22.5 μs | 0.948 |
| track/2. L1 8sat/5K – track! OneBit | 15.8 μs | 16.4 μs | 0.967 |
| track/2. L1 8sat/5K – track!-threaded | 49.3 μs | 51.5 μs | 0.959 |
| track/2. L1 8sat/5K – track-threaded | 53.2 μs | 53.5 μs | 0.995 |
| track/3. E1B 4sat/25K – track | 136.0 μs | 140.0 μs | 0.967 |
| track/3. E1B 4sat/25K – track! | 140.0 μs | 140.0 μs | 1.0 |
| track/3. E1B 4sat/25K – track! Int16 | 60.2 μs | 60.8 μs | 0.991 |
| track/3. E1B 4sat/25K – track!-threaded | 136.0 μs | 145.0 μs | 0.934 |
| track/3. E1B 4sat/25K – track-threaded | 136.0 μs | 140.0 μs | 0.967 |
| track/4. 8L1+8E1B/25K – track | 490.0 μs | 526.0 μs | 0.932 |
| track/4. 8L1+8E1B/25K – track! | 506.0 μs | 500.0 μs | 1.01 |
| track/4. 8L1+8E1B/25K – track!-threaded | 524.0 μs | 489.0 μs | 1.07 ✅ |
| track/4. 8L1+8E1B/25K – track-threaded | 490.0 μs | 507.0 μs | 0.966 |
| track/5. multi-signal N=1/5K – track | 6.68 μs | 6.88 μs | 0.97 |
| track/5. multi-signal N=1/5K – track! | 6.8 μs | 6.41 μs | 1.06 ✅ |
| track/6. multi-signal N=2/5K – track | 11.2 μs | 11.1 μs | 1.0 |
| track/6. multi-signal N=2/5K – track! | 10.6 μs | 10.5 μs | 1.01 |
| track/7. multi-signal N=3/5K – track | 12.4 μs | 12.4 μs | 1.0 |
| track/7. multi-signal N=3/5K – track! | 11.7 μs | 12.1 μs | 0.965 |
| track/8. L1CA presync 2 blk – track! | 12.3 μs | 13.3 μs | 0.925 |
| track/8. L1CA presync 20 blk – track! | 120.0 μs | 120.0 μs | 1.0 |
| track/8. L1CA synced 2 blk – track! | 12.2 μs | 12.7 μs | 0.964 |
| track/8. L1CA synced 20 blk – track! | 127.0 μs | 119.0 μs | 1.07 ✅ |
| time_to_load | 153.0 μs | 261.0 μs | 0.585 |
Memory benchmarks (base vs PR head)
| 99d37e1… | 16f259c… | |
|---|---|---|
| downconvert and correlate/CPU/Float32 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Float32 4ant | 2 allocs: 848 B | 2 allocs: 848 B |
| downconvert and correlate/CPU/Float64 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Int16 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Int16 4ant | 2 allocs: 848 B | 2 allocs: 848 B |
| downconvert and correlate/CPU/Int32 | 2 allocs: 576 B | 2 allocs: 576 B |
| fused kernel/1-ant dynamic taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/1-ant static taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/4-ant dynamic taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/4-ant static taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/1-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/1-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/2-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/2-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/4-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/4-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| track/1. Float32/2K – track | 9 allocs: 944 B | 9 allocs: 944 B |
| track/1. Float32/2K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track | 10 allocs: 4656 B | 10 allocs: 4656 B |
| track/2. L1 8sat/5K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track! Int16 | 13 allocs: 1056 B | 13 allocs: 1056 B |
| track/2. L1 8sat/5K – track! OneBit | 45 allocs: 2736 B | 45 allocs: 2736 B |
| track/2. L1 8sat/5K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track-threaded | 10 allocs: 4656 B | 10 allocs: 4656 B |
| track/3. E1B 4sat/25K – track | 10 allocs: 3056 B | 10 allocs: 3056 B |
| track/3. E1B 4sat/25K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/3. E1B 4sat/25K – track! Int16 | 5 allocs: 416 B | 5 allocs: 416 B |
| track/3. E1B 4sat/25K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/3. E1B 4sat/25K – track-threaded | 10 allocs: 3056 B | 10 allocs: 3056 B |
| track/4. 8L1+8E1B/25K – track | 26 allocs: 10464 B | 26 allocs: 10464 B |
| track/4. 8L1+8E1B/25K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/4. 8L1+8E1B/25K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/4. 8L1+8E1B/25K – track-threaded | 26 allocs: 10464 B | 26 allocs: 10464 B |
| track/5. multi-signal N=1/5K – track | 9 allocs: 944 B | 9 allocs: 944 B |
| track/5. multi-signal N=1/5K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/6. multi-signal N=2/5K – track | 9 allocs: 1408 B | 9 allocs: 1408 B |
| track/6. multi-signal N=2/5K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/7. multi-signal N=3/5K – track | 9 allocs: 1760 B | 9 allocs: 1760 B |
| track/7. multi-signal N=3/5K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/8. L1CA presync 2 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| track/8. L1CA presync 20 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| track/8. L1CA synced 2 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| track/8. L1CA synced 20 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| time_to_load | 196 allocs: 13984 B | 196 allocs: 13984 B |
…ts root cause (#198) Investigating #198 (track! "allocates per completed integration") found the reported ~40 KB/completion to be a measurement artifact — `@allocated track!(rand(ComplexF32, n), …)` counts the `rand` input buffer (n×8 B = 40 KB per 5000-sample code period), which grows with signal length. With the buffer hoisted out and measured from a typed call site, track! is flat: the single-threaded backend is 0 B at any completion count (pinned by the existing pre-sync allocation guard at 2 and 20 blocks, on top of #197 which removes the _buffer_find_bit Core.Box that otherwise leaks ~80 B/block under a multi-threaded runtime). The one genuine residual is on the threaded backend: ~64 B per completed code block per group, but only with >1 thread AND >1 satellite, since track! launches Polyester's @Batch once per code block. Correct the docstrings that mislabeled this as an "irreducible ~160 B per system per call" cost, and document the actual root cause: Polyester's @Batch is allocation-free only when the parallel closure touches nothing but Arrays and isbits values; it pays a small per-launch allocation to root any other non-isbits object. The culprit is the GNSS signal (e.g. GPSL1CA is not isbits — it wraps a Matrix code table and a non-isbits SignalLUT), which each satellite's code-replica generation must touch. The only way to remove it is to move code generation to a serial pre-pass (allocation-free but serializes ~30% of the work and slows the threaded pipeline), which is not worth ~64 B; the thread-over-PRNs design is kept. Also cross-reference #198 in the pre-sync allocation guard's rationale (it already pins allocation flat across completion counts). The track! benchmark size is intentionally left at 2000 samples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #198. Documentation-only, on top of #197 (merged), which is the substantive fix. This PR corrects docstrings that mislabeled the threaded allocation, and records the root-cause analysis + prototypes from investigating it.
What #198 turned out to be
The reported "~40 KB per completed integration" is a measurement artifact. The reproducer measures
@allocated track!(rand(ComplexF32, n), …)withn = k·5000; therandinput buffer sits inside the@allocatedblock and is exactlyn·8B = 40 KB per 5000-sample code period, so it grows with completions. Hoisted out and measured from a typed call site,track!is flat 0 B across 1→100 completions, 1 and 11 sats, pre- and post-sync.perf(bit-buffer): fix pre-sync per-code-block allocation in track! (re-target of #194 to master) #197 is the real fix.
_buffer_find_bit'sCore.Boxleaked ~80 B/block (proportional to signal length — exactly the reported symptom). It reproduces on the single-threaded backend (160 B @ 2 blocks → 1600 B @ 20) at any runtime thread count, so the pre-sync guard added in perf(bit-buffer): fix pre-sync per-code-block allocation in track! (re-target of #194 to master) #197 catches it. With perf(bit-buffer): fix pre-sync per-code-block allocation in track! (re-target of #194 to master) #197 the single-threaded path is 0 B.One genuine residual remains, on the threaded backend: ~64 B per completed code block per group, but only with >1 thread AND >1 satellite (so Polyester's
@batchactually distributes work).track!launches one@batchper code block, so it scales with chunk length rather than per call — though it's bounded per call and dwarfed by the input buffer.Root cause (now documented)
Polyester's
@batchroots only bareArrays and isbits values into its worker tasks for free (that's why aVector{Float64}kernel is allocation-free). It pays a small per-launch allocation to root any non-isbitsstruct — and it's the struct-ness that costs, not the contents: capturing a plain struct whose only fields areMatrixes + isbits scalars measures the same ~64 B/launch, while capturing those same bare arrays measures 0 (verified).The culprit is the GNSS signal:
GPSL1CAis not isbits — it wraps aMatrix{Int16}code table and a (also non-isbits)SignalLUT. Each satellite's code-replica generation needs its signal, so every@batchlaunch touches that non-isbits object once. (TheTrackedSatelement is non-isbits for the same reason and is how the loop reaches the signal, so iteratingVector{TrackedSat}isn't free the way iteratingVector{Float64}is.)Prototypes explored (not included — investigation only)
Two ways to reach 0 B were prototyped and measured:
gen_code!(gen from the rawpaddedmatrix, inside the loop)Key findings:
gen_code!exactly in 3/4 spot checks, off by 1 sample in the 4th due to fixed-point-DDA rounding).SignalLUTout ofGPSL1CAdoes not help by itself (it's still a struct). Reaching 0 without a throughput hit needs a GNSSSignals-side bare-arraygen_code!that threads the LUT's arrays + isbits fields to the SIMD resample kernel as separate arguments (never re-wrapped in a struct inside the loop — doing so hits a type-inference cliff and allocates far more). That's a cross-package API change for ~64 B/block, so it's left as a documented follow-up, not done here.Changes in this PR
src/track.jl,src/downconvert_and_correlate_cpu.jl: correct the docstrings that mislabeled the threaded residual as "~160 B per system per call" — it is per code block, only with >1 thread + >1 sat; document the non-isbits-signal root cause and the two 0-B routes (bare-arraygen_code!, or the inferior serial pre-pass).test/track_in_place.jl: cross-reference track! post-sync per-integration allocation (~1.9 KB/completion/sat); original repro was measuring rand() — corrected #198 in the pre-sync allocation guard's rationale (it already pins allocation flat across completion counts).The
track!benchmark size is intentionally left at 2000 samples (no change).🤖 Generated with Claude Code